package engine.gameObject;

import java.util.ArrayList;


import engine.core.scene.Frustum;
import engine.core.scene.Scene;
import engine.gameObject.collider.Collider;
import engine.shader.Shader;
import engine.util.math.Matrix;
import engine.util.math.Quaternion;

/**
 * Represents the standard 3d object.
 * 
 * @author Richard
 * 
 */
public class GameObject {

	//the script, what can be attached by the user
	private Script script = Script.DEFAULT_SCRIPT;
	
	// positioning of the gameobject
	private Quaternion rotation;
	private float[] position;
	private float[] scale;

	// states, whether this objects is rendered in near pane or not
	private boolean inNearPane = true;

	// the corresponding scene
	private Scene scene;

	// a list of child gameobjects
	private ArrayList<GameObject> children;
	//the parent object
	private GameObject parent;

	// the radius required by this object
	private float boundingRadius;

	// the radius, required by the gameobject and its children
	private float surroundingBoundingRadius;
	
	//a string identifying this gameobject. This id may not be unique
	private String id;
	
	//the modelview matrix
	private Matrix modelMatrix;
	
	//collider for this object
	private Collider collider;
	
	//states, if this object is a rigidbody. A rigidbody is checked for collisions against any other objects
	private boolean rigidbody;
	
	//states, if the object should react on collisions
	private boolean colliding;
	
	//states, whether this object is solid or just triggers collisions. If colliding is set to false, then a gameobject is not triggered when intersecting
	private boolean trigger;
	
	// A flag to check if the model matrix must be calculated.
	private boolean calcMat = true;
	
	public GameObject() {
		rotation = new Quaternion(0, 0, 0, 1);
		position = new float[]{0, 0, 0};
		scale = new float[]{1, 1, 1};
		children = new ArrayList<GameObject>();
		
		modelMatrix = new Matrix();
	}
	
	private void calcMatrix() {
		float xx = rotation.getX() * rotation.getX();
	    float xy = rotation.getX() * rotation.getY();
	    float xz = rotation.getX() * rotation.getZ();
	    float xw = rotation.getX() * rotation.getW();

	    float yy = rotation.getY() * rotation.getY();
	    float yz = rotation.getY() * rotation.getZ();
	    float yw = rotation.getY() * rotation.getW();

	    float zz = rotation.getZ() * rotation.getZ();
	    float zw = rotation.getZ() * rotation.getW();

	    modelMatrix.m00 = (1 - 2 * ( yy + zz )) * scale[0];
	    modelMatrix.m01 = 2 * ( xy + zw ) * scale[0];
	    modelMatrix.m02 = 2 * ( xz - yw ) * scale[0];

	    modelMatrix.m10 = 2 * ( xy - zw ) * scale[1];
	    modelMatrix.m11 = (1 - 2 * ( xx + zz )) * scale[1];
	    modelMatrix.m12 = 2 * ( yz + xw ) * scale[1];

	    modelMatrix.m20 = 2 * ( xz + yw ) * scale[2];
	    modelMatrix.m21 = 2 * ( yz - xw ) * scale[2];
	    modelMatrix.m22 = (1 - 2 * ( xx + yy )) * scale[2];

	    modelMatrix.m30 = position[0];
	    modelMatrix.m31 = position[1];
	    modelMatrix.m32 = position[2];
	}

	public void addChild(GameObject child) {
		children.add(child);
		child.setParent(this);
		child.setScene(scene);

		double childRadius = Math.sqrt((child.getPosX() - getPosX())
				* (child.getPosX() - getPosX()) + (child.getPosY() - getPosY())
				* (child.getPosY() - getPosY()) + (child.getPosZ() - getPosZ())
				* (child.getPosZ() - getPosZ())) + child.getSurroundingBoundRadius();
		
		surroundingBoundingRadius = (float)Math.max(surroundingBoundingRadius, childRadius);
	}

	public boolean removeChild(GameObject child) {
		if(children.remove(child)) {
			child.setParent(null);
			child.setScene(null);
			
			return true;
		}
		
		return false;
	}

	public boolean isInNearPane() {
		return inNearPane;
	}

	public void setInNearPane(boolean inNearPane) {
		this.inNearPane = inNearPane;
	}
	
	public void initAbs() {
		init();
		
		for(GameObject child: children) {
			child.initAbs();
		}
	}
	
	public void init() {}

	public void renderAbs(Frustum frustum, Shader shader) {
		if(frustum != null && !frustum.sphereInFrustum(getPosX(), getPosY(), getPosZ(), getSurroundingBoundRadius())) {
			return;
		}
		
		if(calcMat) {
			calcMatrix();
			
			calcMat = false;
		}
		
		render(shader);
		
		for(GameObject child : children) {
			child.renderAbs(frustum, shader);
		}
	}

	public void render(Shader shader) {
	}

	public float[] getPosition() {
		return position;
	}

	public float getPosX() {
		return position[0];
	}

	public float getPosY() {
		return position[1];
	}

	public float getPosZ() {
		return position[2];
	}

	public void setPosition(float[] position) {
		setPosition(position[0], position[1], position[2]);
	}

	public void setPosition(float x, float y, float z) {
		setPositionWithoutUpdate(x, y, z);
		
		updateBoundingSpheres();
	}
	
	private void setPositionWithoutUpdate(float x, float y, float z) {
		// translate childen
		for(GameObject child : children) {
			child.translateWithoutUpdate(x - getPosX(), y - getPosY(), z - getPosZ());
		}
		
		position[0] = x;
		position[1] = y;
		position[2] = z;
		
		calcMat = true;
	}
	
	public void translate(float[] direction) {
		translate(direction[0], direction[1], direction[2]);
	}
	
	public void translate(float x, float y, float z) {
		translateWithoutUpdate(x, y, z);
		
		updateBoundingSpheres();
	}
	
	private void translateWithoutUpdate(float x, float y, float z) {
		// translate children
		for(GameObject child : children) {
			child.translateWithoutUpdate(x, y, z);
		}
		
		position[0] = getPosX() + x;
		position[1] = getPosY() + y;
		position[2] = getPosZ() + z;
		
		calcMat = true;
	}

	public Quaternion getRotation() {
		return rotation;
	}

	public void setRotation(Quaternion rotation) {
		for(GameObject child : children) {
			Quaternion rot = Quaternion.divide(rotation, getRotation()); 
			Quaternion pos = Quaternion.mult(Quaternion.mult(rot, new Quaternion(child.getPosX() - getPosX(), child.getPosY() - getPosY(), child.getPosZ() - getPosZ(), 0)), Quaternion.invert(rot));
			child.setPositionWithoutUpdate(pos.getX() + getPosX(), pos.getY() + getPosY(), pos.getZ()+ getPosZ());

			child.rotate(rot);
		}
		
		this.rotation = rotation;
		
		calcMat = true;
	}

	public void rotate(Quaternion rotation) {
		for(GameObject child : children) {
			Quaternion pos = Quaternion.mult(Quaternion.mult(rotation, new Quaternion(child.getPosX() - getPosX(), child.getPosY() - getPosY(), child.getPosZ() - getPosZ(), 0)), Quaternion.invert(rotation));
			child.setPositionWithoutUpdate(pos.getX() + getPosX(), pos.getY() + getPosY(), pos.getZ()+ getPosZ());
			
			child.rotate(rotation);
		}
		
		this.rotation = Quaternion.mult(rotation, this.rotation);
		
		calcMat = true;
	}

	public float[] getScale() {
		return scale;
	}
	
	public float getScaleX() {
		return scale[0];
	}

	public float getScaleY() {
		return scale[1];
	}
	
	public float getScaleZ() {
		return scale[2];
	}
	
	public void setScale(float[] scale) {
		setScale(scale[0], scale[1], scale[2]);
	}

	public void setScale(float x, float y, float z) {
		// scale children
		for(GameObject child : children) {
			child.translateWithoutUpdate((child.getPosX() - getPosX()) * 
					(x != 0 ? (1.0f - getScaleX() / x) : 0), 
					(child.getPosY() - getPosY()) * 
					(y != 0 ? (1.0f - getScaleY() / y) : 0), 
					(child.getPosZ() - getPosZ()) * 
					(z != 0 ? (1.0f - getScaleZ() / z) : 0));
			child.scaleWithoutUpdate((getScaleX() != 0 ? x / getScaleX() : 0), 
					(getScaleY() != 0 ? y / getScaleY() : 0), 
					(getScaleZ() != 0 ? z / getScaleZ() : 0));
		}
		
		boundingRadius /= Math.max(getScaleX(), Math.max(getScaleY(), getScaleZ()));
		scale[0] = x;
		scale[1] = y;
		scale[2] = z;
		boundingRadius *= Math.max(x, Math.max(y, z));
		
		calcMat = true;
		
		updateBoundingSpheres();
	}

	public void scale(float[] scale) {
		scale(scale[0], scale[1], scale[2]);
	}
	
	public void scale(float x, float y, float z) {
		scaleWithoutUpdate(x, y, z);
		
		updateBoundingSpheres();
	}
	
	private void scaleWithoutUpdate(float x, float y, float z) {
		// scale children
		for(GameObject child : children) {
			child.translateWithoutUpdate((getPosX() - child.getPosX()) * (1.0f - x), 
					(getPosY() - child.getPosY()) * (1.0f - y), 
					(getPosZ() - child.getPosZ()) * (1.0f - z));
			child.scaleWithoutUpdate(x, y, z);
		}
		
		scale[0] = getScaleX() * x;
		scale[1] = getScaleY() * y;
		scale[2] = getScaleZ() * z;
		boundingRadius *= Math.max(x, Math.max(y, z));
		
		calcMat = true;
	}
	
	public Scene getScene() {
		return scene;
	}

	public void setScene(Scene scene) {
		this.scene = scene;

		for (GameObject child : children) {
			child.setScene(scene);
		}
	}

	public float getBoundingRadius() {
		return boundingRadius;
	}

	public void setBoundingRadius(float boundingRadius) {
		this.boundingRadius = boundingRadius;

		surroundingBoundingRadius = (float)Math.max(boundingRadius,
				surroundingBoundingRadius);
	}

	public float getSurroundingBoundRadius() {
		return surroundingBoundingRadius;
	}

	private void updateBoundingRadius() {
		float maxSquareRadius = boundingRadius * boundingRadius;
		float x, y, z;

		for (GameObject child : children) {
			x = Math.abs(getPosX() - child.getPosX()) + child.getSurroundingBoundRadius();
			y = Math.abs(getPosY() - child.getPosY()) + child.getSurroundingBoundRadius();
			z = Math.abs(getPosZ() - child.getPosZ()) + child.getSurroundingBoundRadius();
			
			maxSquareRadius = Math.max(maxSquareRadius, x * x + y * y + z * z);
		}

		surroundingBoundingRadius = (float)Math.sqrt(maxSquareRadius);
	}

	private void updateBoundingSpheres() {
		updateBoudingRadiusBottomUp();
		updateBoundingRadiusTopDown();
	}
	
	private void updateBoudingRadiusBottomUp() {
		updateBoundingRadius();
		
		if(getParent() != null) {
			getParent().updateBoudingRadiusBottomUp();
		}
	}
	
	private void updateBoundingRadiusTopDown() {
		if(collider != null) {
			collider.reset();
		}
		
		for(GameObject child: children) {
			child.updateBoundingRadiusTopDown();
		}
	}

	public void updateAbs(float timeElapsed) {
		update(timeElapsed);
		script.update(timeElapsed);
		
		for(GameObject child: children) {
			child.updateAbs(timeElapsed);
		}
	}

	public void update(float timeElapsed) {
	}

	public void keyPressedAbs(int key) {
		keyPressed(key);
		script.keyPressed(key);

		for (GameObject child : children) {
			child.keyPressedAbs(key);
		}
	}

	public void keyPressed(int key) {
	}

	public void keyReleasedAbs(int key) {
		keyReleased(key);
		script.keyReleased(key);

		for (GameObject child : children) {
			child.keyReleasedAbs(key);
		}
	}

	public void keyReleased(int key) {
	};

	public void mousePressedAbs(int posX, int posY, int button) {
		mousePressed(posX, posY, button);
		script.mousePressed(posX, posY, button);

		for (GameObject child : children) {
			child.mousePressedAbs(posX, posY, button);
		}
	}

	public void mousePressed(int posX, int posY, int button) {
	}

	public void mouseReleasedAbs(int posX, int posY, int button) {
		mouseReleased(posX, posY, button);
		script.mouseReleased(posX, posY, button);

		for (GameObject child : children) {
			child.mouseReleasedAbs(posX, posY, button);
		}
	}

	public void mouseReleased(int posX, int posY, int button) {
	}

	public void mouseMovedAbs(int posX, int posY, int button) {
		mouseMoved(posX, posY, button);
		script.mouseMoved(posX, posY, button);

		for (GameObject child : children) {
			child.mouseMovedAbs(posX, posY, button);
		}
	}

	public void mouseMoved(int posX, int posY, int button) {
	}

	public void mouseDraggedAbs(int posX, int posY, int button) {
		mouseDragged(posX, posY, button);
		script.mouseDragged(posX, posY, button);

		for (GameObject child : children) {
			child.mouseDraggedAbs(posX, posY, button);
		}
	}

	public void mouseDragged(int posX, int posY, int button) {
	}

	public void mouseWheelMovedAbs(int movement) {
		mouseWheelMoved(movement);
		script.mouseWheelMoved(movement);

		for (GameObject child : children) {
			child.mouseWheelMovedAbs(movement);
		}
	}

	public void mouseWheelMoved(int movement) {
	}
	
	public ArrayList<GameObject> getChildren() {
		return children;
	}
	
	public GameObject getParent() {
		return parent;
	}
	
	public void setParent(GameObject parent) {
		this.parent = parent;
	}

	public String getId() {
		return id;
	}
	
	public void setId(String id) {
		this.id = id;
	}
	
	public Matrix getModelMatrix() {
		return modelMatrix;
	}
	
	public void copyTo(GameObject gameObject) {
		gameObject.setPosition(getPosition());
		gameObject.setScale(getScale());
		gameObject.setRotation(getRotation());
		gameObject.setBoundingRadius(boundingRadius);
		
		if(collider != null) {
			gameObject.setCollider(collider.copy());
		}
		
		for(GameObject child : children) {
			gameObject.addChild(child.copy());
		}
	}
	
	public GameObject copy() {
		GameObject gameObject = new GameObject();
		
		copyTo(gameObject);
		
		return gameObject;
	}

	public boolean isRigidbody() {
		return rigidbody;
	}

	public void setRigidbody(boolean rigidbody) {
		this.rigidbody = rigidbody;
		
		if(rigidbody) {
			setColliding(true);
		}
	}
	
	public void setCollider(Collider collider) {
		this.collider = collider;
	}
	
	public Collider getCollider() {
		return collider;
	}
	
	public boolean checkCollision(GameObject gameObject) {
		if(gameObject == this) {
			return false;
		}
		
		if(!Collider.sphereInSphere(gameObject.getPosition(), gameObject.getSurroundingBoundRadius(), getPosition(), getSurroundingBoundRadius())) {
			return false;
		}
		
		if(collider != null && gameObject.isColliding() && isColliding()) {
			if(collider.collides(this, gameObject)) {
				return true;
			}
		}
		
		for(GameObject child: children) {
			if(child.checkCollision(gameObject)) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * A callback function for collisions which must be implemented in the gameobject itself. 
	 * @param other
	 * @param upVector
	 */
	public void collidesAbs(GameObject other, float[] upVector) {
		collides(other, upVector);
		script.collides(other, upVector);
	}
	
	public void collides(GameObject other, float[] upVector) {}

	public boolean isColliding() {
		return colliding;
	}

	public void setColliding(boolean colliding) {
		this.colliding = colliding;
	}

	public boolean isTrigger() {
		return trigger;
	}

	public void setTrigger(boolean trigger) {
		this.trigger = trigger;
	}
	
	/**
	 * This function is used as a callack, when a rigidody collides with an object, which has a trigger on itself
	 * @param other
	 * @param upVector
	 */
	public void triggered(GameObject other, float[] upVector) {}

	public Script getScript() {
		return script;
	}

	public void setScript(Script script) {
		if(this.script != null) {
			this.script.setGameObject(null);
		}
		
		if(script == null) {
			this.script = Script.DEFAULT_SCRIPT;
		} else {
			this.script = script;
		}
		
		if(script != null) {
			script.setGameObject(this);
		}
	}
}
